import os
import ly.dom

def merge_by_id(parent, child):
    if type(parent) is list and type(child) is list:
        merged = child.copy()
        for i, m in enumerate(merged):
            merged[i] = merge_by_id(parent, m)
        return merged
    elif type(parent) is list and type(child) is dict:
        for i in parent:
            if type(i) is dict and 'id' in i and 'id' in child and i['id'] == child['id']:
                return merge_by_depth(i, child)
        if 'contents' in child:
            merged = child.copy()
            merged['contents'] = merge_by_id(parent, child['contents'])
            return merged

def merge_by_depth(parent, child):
    if type(parent) is dict and type(child) is dict:
        merged = {}
        for i in parent:
            if i in child:
                merged.update({i: merge_by_depth(parent[i], child[i])})
            else:
                merged.update({i: parent[i]})
        for i in child:
            if i not in parent:
                merged.update({i: child[i]})
        return merged
    elif type(parent) is dict and type(child) is list:
        merged = []
        for i in child:
            if type(i) is dict:
                merged.append(merge_by_depth(parent, i))
        return merged
    elif type(parent) is list and type(child) is list:
        merged = parent.copy()
        for c in child:
            in_parent = False
            for i, m in enumerate(merged):
                if type(c) is dict and type(m) is dict and 'id' in c and 'id' in m and c['id'] == m['id']:
                    merged[i] = merge_by_depth(m, c)
                    in_parent = True
            if not in_parent:
                merged.append(c)
        return merged
    else:
        return child

def assign_to_container(container, key, value):
    if key is None:
        container.append(ly.dom.Text(value))
    elif key[0] == '\\':
        container.append(ly.dom.Text(key + ' ' + value))
    elif value[0] == '#':
        container[key] = ly.dom.Scheme(value[1:])
    else:
        container[key] = value

def make_staff_or_staff_group(children, parent, movement, settings):
    for staff_or_staff_group in children.copy():
        # check predefined instruments
        if ('instruments' in settings
                and 'id' in staff_or_staff_group 
                and staff_or_staff_group['id'] in settings['instruments']):
            instrument = settings['instruments'][staff_or_staff_group['id']].copy()
            instrument.update(staff_or_staff_group)
            staff_or_staff_group = instrument

        if 'id' in staff_or_staff_group:
            name = staff_or_staff_group['id']
        else:
            name = None

        # \with
        with_statement = ly.dom.With()
        for key, value in {
                'name': ('instrumentName', '{}'),
                'size': (None, '\\magnifyStaff #{}')
                }.items():
            if key in staff_or_staff_group:
                assign_to_container(with_statement, value[0], value[1].format(staff_or_staff_group[key]))
        if 'with' in staff_or_staff_group:
            for key, value in staff_or_staff_group['with'].items():
                assign_to_container(with_statement, key, value)

        if 'type' in staff_or_staff_group:
            staff_types = {
                    'Staff': ly.dom.Staff,
                    'DrumStaff': ly.dom.DrumStaff,
                    'RhythmicStaff': ly.dom.RhythmicStaff,
                    'TabStaff': ly.dom.TabStaff}
            staff_group_types = {
                    'StaffGroup': ly.dom.StaffGroup,
                    'ChoirStaff': ly.dom.ChoirStaff,
                    'GrandStaff': ly.dom.GrandStaff,
                    'PianoStaff': ly.dom.PianoStaff}

            # \new Staff, DrumStaff, etc.
            if staff_or_staff_group['type'] in staff_types:
                staff = staff_types[staff_or_staff_group['type']](name)
                parent.append(staff)
                staff.append(with_statement)

                # \relative { }
                if staff_or_staff_group['type'] in [
                        'Staff',
                        'TabStaff']:
                    relative = ly.dom.Relative()
                    staff.append(relative)
                    relative.append(ly.dom.Pitch(octave=0, note=0))
                    seq = ly.dom.Seq()
                    relative.append(seq)

                # \drummode { }
                elif staff_staff_group['type'] == 'DrumStaff':
                    drummode = ly.dom.DrumMode()
                    staff.append(drummode)
                    seq = ly.dom.Seq()
                    drummode.append(seq)

                # { }
                else:
                    seq = ly.dom.Seq()
                    staff.append(seq)

                if 'id' in staff_or_staff_group:
                    # \include
                    with open('lilypond/' + movement['id'] + '/' + staff_or_staff_group['id'] + '_music.ily', 'a') as f:
                        pass
                    seq.append(ly.dom.Include(movement['id'] + '/' + staff_or_staff_group['id'] + '_music.ily'))

                    # \addlyrics
                    if 'lyrics' in staff_or_staff_group and staff_or_staff_group['lyrics']:
                        addlyrics = ly.dom.AddLyrics()
                        staff.append(addlyrics)
                        with open('lilypond/' + movement['id']
                                + '/' + staff_or_staff_group['id']
                                + '_lyrics.ily', 'a') as f:
                            pass
                        addlyrics.append(ly.dom.Include(movement['id'] + '/' + staff_or_staff_group['id'] + '_lyrics.ily'))

            # \new StaffGroup, PianoStaff, etc.
            if staff_or_staff_group['type'] in staff_group_types:
                staff_group = staff_group_types[staff_or_staff_group['type']](name)
                parent.append(staff_group)
                staff_group.append(with_statement)
                sim = ly.dom.Sim()
                staff_group.append(sim)
                if 'contents' in staff_or_staff_group:
                    make_staff_or_staff_group(staff_or_staff_group['contents'], sim, movement, settings)

def make_score(book_statement, book, movement, settings):
    score = ly.dom.Score()
    book_statement.append(score)

    # make directories
    if not os.path.isdir('./lilypond/' + movement['id']):
        os.makedirs('./lilypond/' + movement['id'])

    # \header
    header = ly.dom.Header()
    score.append(header)
    if 'header' in movement:
        for key, value in movement['header'].items():
            header[key] = value

    # << >>
    sim = ly.dom.Sim()
    score.append(sim)

    make_staff_or_staff_group(book['instruments'], sim, movement, settings)

def make_book(book, page_size, settings):
    document = ly.dom.Document()

    # \version
    if 'lilypond_version' in settings:
        ly.dom.Version(settings['lilypond_version'], document)

    # \language
    if 'language' in settings:
        ly.dom.Line('\\language "{0}"'.format(settings['language']), document)

    ly.dom.BlankLine(document)

    # \book
    book_statement = ly.dom.Book()
    document.append(book_statement)

    # \paper
    paper = ly.dom.Paper()
    if 'paper' in settings:
        for key, value in settings['paper'].items():
            assign_to_container(paper, key, value)
    if page_size:
        assign_to_container(paper, None, '#(set-paper-size "{}")'.format(page_size['id']))
    if len(paper):
        book_statement.append(paper)

    # \header
    header = ly.dom.Header()
    book_statement.append(header)
    if 'header' in settings:
        for key, value in settings['header'].items():
            header[key] = value

    # \score
    if 'movements' in settings:
        movements = settings['movements']
    else:
        movements = None
    for movement in movements:
        make_score(book_statement, book, movement, settings)

    return ly.dom.Printer().indent(document)
